﻿using Dapper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CNative.Utilities;
namespace CNative.Dapper.Utils
{
    /// <summary>
    /// DapperSQL执行跟踪
    /// </summary>
    public class SqlMapperTrace
    {
        /// <summary>
        /// Sql执行拦截事件
        /// </summary>
        static Action<TraceInfo> BeforeSqlCommand = null;
        /// <summary>
        /// SQL运行成功后拦截事件
        /// </summary>
        static Action<TraceInfo> AfterSqlCommand = null;
        /// <summary>
        /// 设置DapperSql执行拦截事件
        /// </summary>
        /// <param name="beforeExecuteTrace">执行前事件</param>
        /// <param name="afterExecuteTrace">执行后事件</param>
        public static void SetMapperTrace(Action<TraceInfo> beforeExecuteTrace, Action<TraceInfo> afterExecuteTrace)
        {
            //if (null != BeforeSqlCommand)
            //    return;
            BeforeSqlCommand = beforeExecuteTrace;
            AfterSqlCommand = afterExecuteTrace;

        }
        /// <summary>
        /// 执行SQL运行前拦截事件
        /// </summary>
        /// <param name="traceInfo"></param>
        internal static void ShellBeforeCommandExecute(TraceInfo traceInfo)
        {
            if (null != BeforeSqlCommand)
                BeforeSqlCommand(traceInfo);
        }
        /// <summary>
        /// 执行SQL运行前拦截事件
        /// </summary>
        /// <param name="traceInfo"></param>
        internal static TraceInfo ShellBeforeCommandExecute(SqlEntity sqlEntity)
        {
            try
            {
                if (sqlEntity.IsNullOrEmpty() || (null == BeforeSqlCommand && null == AfterSqlCommand))
                    return null;
                var traceInfo = new TraceInfo()
                {
                    Token = sqlEntity.GetHashCode().ToString(),
                    CommandText = sqlEntity.Sql,
                    StartTime = DateTime.Now,
                    SqlParams = sqlEntity.Parameters ?? sqlEntity.Parameter,
                    State = SqlState.Start,
                    DBName = sqlEntity?.DbHelper?.DBName,
                    sqlEntity = sqlEntity
                };
                ShellBeforeCommandExecute(traceInfo);
                return traceInfo;
            }
            catch { }
            return null;
        }
        /// <summary>
        /// 执行SQL运行成功后拦截事件，若SQL执行失败，则此方法不会触发
        /// </summary>
        /// <param name="traceInfo"></param>
        internal static void ShellAfterCommandExecute(TraceInfo traceInfo, Exception ex = null)
        {
            try
            {
                if (traceInfo.IsNullOrEmpty() || null == AfterSqlCommand) return;
                traceInfo.Calculate();
                traceInfo.EndTime = DateTime.Now;
                traceInfo.State = ex.IsNullOrEmpty() ? SqlState.End : SqlState.Error;
                traceInfo.IsDbSucc = ex.IsNullOrEmpty();
                traceInfo.Message = ex?.ToString();

                AfterSqlCommand?.Invoke(traceInfo);
            }
            catch { }
        }
        ///// <summary>
        ///// 执行SQL运行成功后拦截事件，若SQL执行失败，则此方法不会触发
        ///// </summary>
        ///// <param name="traceInfo"></param>
        //internal static void ShellAfterCommandExecute(SqlEntity sqlEntity, string ErrorMsg = "")
        //{
        //    if (sqlEntity.IsNullOrEmpty() || null == AfterSqlCommand) return;
        //    ShellAfterCommandExecute(new TraceInfo()
        //    {
        //        Token = sqlEntity.GetHashCode().ToString(),
        //        CommandText = sqlEntity.Sql,
        //        ExecuteTime = DateTime.Now,
        //        SqlParams = sqlEntity.Parameters ?? sqlEntity.Parameter,
        //        IsStart = ErrorMsg.IsNullOrEmpty() ? SqlState.End : SqlState.Error,
        //        IsDbSucc = ErrorMsg.IsNullOrEmpty(),
        //        Message = ErrorMsg,
        //        DBName = sqlEntity?.DbHelper?.DBName
        //    });
        //}
        /// <summary>
        /// 执行SQL运行成功后拦截事件，若SQL执行失败，则此方法不会触发
        /// </summary>
        /// <param name="traceInfo"></param>
        //internal static void ShellAfterCommandExecute(TraceInfo traceInfo)
        //{
        //    if (null != AfterSqlCommand)
        //        AfterSqlCommand(traceInfo);
        //}
    }

    /// <summary>
    /// Sql执行状态
    /// </summary>
    public enum SqlState
    {
        /// <summary>
        /// 执行中
        /// </summary>
        Start,
        /// <summary>
        /// 完成
        /// </summary>
        End,
        /// <summary>
        /// 失败
        /// </summary>
        Error
    }
    /// <summary>
    /// 计算方法执行时间
    /// </summary>
    public class StopwatchWrapper
    {
        private readonly System.Diagnostics.Stopwatch _stopwatch;
        /// <summary>
        /// Initializes a new Stopwatch instance, sets the elapsed time property to zero, and starts measuring elapsed time.
        /// </summary>
        /// <returns>The <see cref="IStopwatch"/>.</returns>
        public static StopwatchWrapper StartNew() => new StopwatchWrapper();
        /// <summary>
        /// 默认构造
        /// </summary>
        /// <summary>
        /// Prevents a default instance of the <see cref="StopwatchWrapper"/> class from being created.
        /// </summary>
        private StopwatchWrapper()
        {
            _stopwatch = System.Diagnostics.Stopwatch.StartNew();
        }

        /// <summary>
        /// Gets the total elapsed time measured by the current instance, in timer ticks.
        /// </summary>
        public long ElapsedTicks => _stopwatch.ElapsedTicks;

        /// <summary>
        /// Gets the frequency of the timer as the number of ticks per second. This field is read-only.
        /// </summary>
        public long Frequency => System.Diagnostics.Stopwatch.Frequency;

        /// <summary>
        /// Gets a value indicating whether the Stopwatch timer is running.
        /// </summary>
        public bool IsRunning => _stopwatch.IsRunning;

        /// <summary>
        /// Stops measuring elapsed time for an interval.
        /// </summary>
        public void Stop() => _stopwatch.Stop();

        /// <summary>
        /// 计算方法执行时间
        /// </summary>
        /// <returns></returns>
        public double TotalSeconds
        {
            get
            {
                try
                {
                    if (IsRunning)
                        Stop();
                    return _stopwatch.Elapsed.TotalSeconds;
                }
                catch { return 0; }
            }
        }
        /// <summary>
        /// 计算方法执行时间
        /// </summary>
        /// <returns></returns>
        public double TotalMilliseconds
        {
            get
            {
                try
                {
                    if (IsRunning)
                        Stop();
                    return _stopwatch.Elapsed.TotalMilliseconds;
                }
                catch { return 0; }
            }
        }
    }
    /// <summary>
    /// 跟踪信息
    /// </summary>
    public class TraceInfo
    {
        private readonly StopwatchWrapper _stopwatch;
        public TraceInfo()
        {
            _stopwatch = StopwatchWrapper.StartNew();
        }
        /// <summary>
        /// 操作识别Key，用用跟踪同一SQL是否执行成功
        /// </summary>
        public string Token { get; set; }
        public string DBName { get; set; }
        public SqlEntity sqlEntity { get; set; }
        /// <summary>
        /// SQL语句
        /// </summary>
        public String CommandText { get; set; }
        /// <summary>
        /// sql参数
        /// </summary>
        public object SqlParams { get; set; }

        /// <summary>
        /// 是否正在启动
        /// </summary>
        public SqlState State { get; set; }
        /// <summary>
        /// 开始执行时间，性能统计，自己可根据两次时间差去实现
        /// </summary>
        public DateTime StartTime { get; set; }
        /// <summary>
        /// 结束执行时间，性能统计，自己可根据两次时间差去实现
        /// </summary>
        public DateTime EndTime { get; set; }

        /// <summary>
        /// 是否执行数据库成功,主要针对增删改操作
        /// </summary>
        public bool IsDbSucc { get; set; }

        /// <summary>
        /// 提示信息
        /// </summary>
        public string Message { get; set; }

        double _TotalSeconds = 0;
        /// <summary>
        /// 执行耗时
        /// </summary>
        /// <returns></returns>
        public double TotalSeconds
        {
            get
            {
                //if (_TotalSeconds == 0) Calculate();
                return _TotalSeconds;
            }
            set { _TotalSeconds = value; }
        }
        double _TotalMilliseconds = 0;
        /// <summary>
        /// 执行耗时
        /// </summary>
        /// <returns></returns>
        public double TotalMilliseconds
        {
            get
            {
                //if (_TotalMilliseconds == 0) Calculate();
                return _TotalMilliseconds;
            }
            set { _TotalMilliseconds = value; }
        }
        string _FormatSql = "";
        /// <summary>
        /// 格式化后的sql脚本
        /// </summary>
        /// <returns></returns>
        public string FormatedSql
        {
            get
            {
                if (_FormatSql.IsNullOrEmpty())
                    _FormatSql = GetFormatSql();
                return _FormatSql;
            }
        }
        /// <summary>
        /// 计算方法执行时间
        /// </summary>
        /// <returns></returns>
        public double Calculate()
        {
            if (_TotalSeconds == 0)
                _TotalSeconds = _stopwatch.TotalSeconds;
            if (_TotalMilliseconds == 0)
                _TotalMilliseconds = _stopwatch.TotalMilliseconds;
            return _TotalSeconds;
        }
        public override string ToString()
        {
            return "State:" + State + ",Token:" + Token
                + (State == SqlState.Start ? ",StartTime:" + StartTime.ToString("yyyy-MM-dd HH:mm:ss ffff")
                : ",EndTime:" + EndTime.ToString("yyyy-MM-dd HH:mm:ss ffff")
                + ",TotalMilliseconds:" + TotalMilliseconds + "ms"
                + ",IsSuccess:" + IsDbSucc 
                + (IsDbSucc ? "" : ",Message:" + Message)
                + ",DBName:" + DBName + "\r\nCommandText:" + FormatedSql);
        }
        #region GetFormatSql
        protected virtual string GetFormatSql()
        {
            var buffer = new StringBuilder();

            // only treat 'StoredProcedure' differently since 'Text' may contain 'TableDirect' or 'StoredProcedure'
            if (sqlEntity?.CommandType == System.Data.CommandType.StoredProcedure)
            {
                GenerateStoreProcedureCall(buffer);
            }
            else
            {
                GenerateSqlText(buffer);
            }
            TerminateSqlStatement(buffer);
            return buffer.ToString();
        }
        private void GenerateStoreProcedureCall(StringBuilder buffer)
        {
            buffer.Append(CommandText).Append(" (");

            var sql = "";
            if (SqlParams != null)
            {
                if (SqlParams is DynamicParameters)
                {
                    var DyParams = (SqlParams as DynamicParameters);
                    var pamns2 = DyParams?.ParameterNames;
                    pamns2.ForEach(item =>
                    {
                        var pval = DyParams.Get<object>(item);
                        sql += SqlReplace(item, pval);
                    });
                }
                else if (SqlParams is IEnumerable<KeyValuePair<string, object>>)
                {
                    var pamns3 = (SqlParams as IEnumerable<KeyValuePair<string, object>>);
                    pamns3.ForEach(item =>
                    {
                        sql += SqlReplace(item.Key, item.Value);
                    });
                }
                else
                {
                    var properties = SqlParams.GetPropertyNames();
                    properties?.ForEach(item =>
                    {
                        var pval = FastReflection.FastGetPropertyValue(item, SqlParams);
                        sql += SqlReplace(item, pval);
                    });
                    var fields = SqlParams.GetFieldNames();
                    fields?.ForEach(item =>
                    {
                        var pval = FastReflection.FastGetFieldValue(item, SqlParams);
                        sql += SqlReplace(item, pval);
                    });
                }
            }
            buffer.Append(sql.TrimEnd(',')).Append(");");
        }
        /// <summary>
        /// Generate formatter output text for all <paramref name="parameters"/>.
        /// </summary>
        /// <param name="buffer"><see cref="StringBuilder"/> to use</param>
        /// <param name="parameters">Parameters to evaluate</param>
        protected void GenerateSqlText(StringBuilder buffer)
        {
            string sql = CommandText;
            if (SqlParams != null)
            {
                if (SqlParams is DynamicParameters)
                {
                    var DyParams = (SqlParams as DynamicParameters);
                    var pamns2 = DyParams?.ParameterNames;
                    pamns2.ForEach(item =>
                    {
                        var pval = DyParams.Get<object>(item);
                        sql = SqlReplace(sql, item, pval);
                    });
                }
                else if (SqlParams is IEnumerable<KeyValuePair<string, object>>)
                {
                    var pamns3 = (SqlParams as IEnumerable<KeyValuePair<string, object>>);
                    pamns3.ForEach(item =>
                    {
                        sql = SqlReplace(sql, item.Key, item.Value);
                    });
                }
                else
                {
                    var properties = SqlParams.GetPropertyNames();
                    properties?.ForEach(item =>
                    {
                        var pval = FastReflection.FastGetPropertyValue(item, SqlParams);
                        sql = SqlReplace(sql, item, pval);
                    });
                    var fields = SqlParams.GetFieldNames();
                    fields?.ForEach(item =>
                    {
                        var pval = FastReflection.FastGetFieldValue(item, SqlParams);
                        sql = SqlReplace(sql, item, pval);
                    });
                }
            }
            buffer.Append(sql);
        }
        private string SqlReplace(string fieldName, object pval)
        {
            if (pval.IsNotNullOrEmpty())
            {
                return PrepareValue(pval) + ",";// (EnsureParameterPrefix(fieldName)+"="+ PrepareValue(pval));
            }
            else
            {
                return "null,";// (EnsureParameterPrefix(fieldName)+ "=null");
            }
        }
        private string SqlReplace(string sql, string fieldName, object pval)
        {
            if (pval.IsNotNullOrEmpty())
            {
                if (sql.Contains('?'))
                    sql = Funs.Replace(sql, "?", PrepareValue(pval));
                else
                    sql = sql.Replace(EnsureParameterPrefix(fieldName), PrepareValue(pval));
            }
            else
            {
                if (sql.Contains('?'))
                    sql = Funs.Replace(sql, "?", "null");
                else
                    sql = sql.Replace(EnsureParameterPrefix(fieldName), "null");
            }
            return sql;
        }
        /// <summary>
        /// What data types should not be quoted when used in parameters
        /// </summary>
        protected static readonly string[] DontQuote =
        {
            "Int16", "Int32", "Int64", "UInt16", "UInt32", "UInt64",
            "Int", "UInt", "Short", "Long", "Float",
            "Boolean", "Byte", "SByte", "Byte[]",
            "Double", "Single", "Currency", "Decimal"
        };
        /// <summary>
        /// Prepare the parameter value for use in SqlFormatter output
        /// </summary>
        /// <param name="parameter"></param>
        /// <returns></returns>
        protected string PrepareValue(object parameterValue)
        {
            if (parameterValue == null)
            {
                return "null";
            }
            var vType = parameterValue.GetType().Name;
            if (vType.In("Boolean", "Bool", "bool"))
            {
                return parameterValue.ToString() == "True" ? "1" : "0";
            }
            return parameterValue.ToSqlValue().NullToStr();
            //if (DontQuote.Contains(vType, StringComparer.OrdinalIgnoreCase))
            //{
            //    if (vType.In("Boolean", "Bool", "bool"))
            //    {
            //        return parameterValue.ToString() == "True" ? "1" : "0";
            //    }

            //    return parameterValue.ToString();
            //}
            //var prefix = string.Empty;
            ////if (vType == "String" || vType == "StringFixedLength")
            ////{
            ////    prefix = "N";
            ////}

            //return prefix + "'" + parameterValue.ToString().Replace("'", "''") + "'";
        }
        private string EnsureParameterPrefix(string name) =>
           !name.StartsWith(sqlEntity.DbHelper.SqlDbProvider.ParamKeyword, StringComparison.Ordinal) ? sqlEntity.DbHelper.SqlDbProvider.ParamKeyword + name : name;

        private string RemoveParameterPrefix(string name) =>
            name.StartsWith(sqlEntity.DbHelper.SqlDbProvider.ParamKeyword, StringComparison.Ordinal) ? name.Substring(1) : name;
        /// <summary>
        /// This function is necessary to always return the sql statement terminated with a semicolon.
        /// Since we're using semicolons, we should also add it to the end.
        /// </summary>
        /// <param name="sqlStatement">The SQL statement to terminate if necessary</param>
        private void TerminateSqlStatement(StringBuilder sqlStatement)
        {
            if (sqlStatement[sqlStatement.Length - 1] != ';')
            {
                sqlStatement.Append(';');
            }
        }
        #endregion
    }

}